﻿namespace TransitDataParser
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using System.Globalization;
    using Caltrain.Model;

    /// <summary>
    /// General Transit File Specification Parser
    /// </summary>
    class GTFSParser
    {
        /// <summary>
        /// CSV Line Parser
        /// </summary>
        private ICSVLineParser csvLineParser;

        /// <summary>
        /// Initializes an instance of this class
        /// </summary>
        /// <param name="csvLineParser">CSV Line parser to use</param>
        public GTFSParser(ICSVLineParser csvLineParser)
        {
            this.csvLineParser = csvLineParser;
        }

        /// <summary>
        /// Parse stop data
        /// </summary>
        /// <param name="filename">File with stop data</param>
        /// <returns>Dictionary that maps stop id to data</returns>
        public Dictionary<string, Stop> ParseStops(string filename)
        {
            int stopId = 1;
            return this.ParseFile<string, Stop, Stop>(
                filename,
                (stopData) =>
                {
                    return new Stop()
                    {
                        Id = stopData["stop_id"],
                        Latitude = Double.Parse(stopData["stop_lat"]),
                        Longitude = Double.Parse(stopData["stop_lon"]),
                        LookupId = stopId++,
                        ZoneId = Int32.Parse(stopData["zone_id"])
                    };
                },
                (stop) => stop.Id,
                (stop, s) => stop);
        }

        /// <summary>
        /// Parse calendar data
        /// </summary>
        /// <param name="calendarFile">File with calendar data</param>
        /// <param name="calendarDatesFile">File with exception data</param>
        /// <returns>Calendar data</returns>
        public Caltrain.Model.Calendar ParseCalendar(string calendarFile, string calendarDatesFile)
        {
            // Parse calendar exception
            Service[] services = this.ParseFile<string, Service, Service>(
                calendarFile,
                (serviceData) =>
                {
                    return new Service()
                    {
                        Id = serviceData["service_id"],
                        Days = this.ParseServiceDays(serviceData),
                        StartDate = DateTime.ParseExact(serviceData["start_date"], "yyyyMMdd", CultureInfo.InvariantCulture),
                        EndDate = DateTime.ParseExact(serviceData["end_date"], "yyyyMMdd", CultureInfo.InvariantCulture) + new TimeSpan(23, 59, 59)
                    };
                },
                (service) => service.Id,
                (service, s) => service)
            .Values
            .ToArray();

            // TODO: Parse exception data
            Caltrain.Model.Calendar calendar = new Caltrain.Model.Calendar()
            {
                Services = services,
                Exceptions = new Dictionary<DateTime, ServiceException>()
            };

            return calendar;
        }

        /// <summary>
        /// Parse trip data
        /// </summary>
        /// <param name="filename">file with trip data</param>
        /// <returns>Dictionary that maps trip id to trip data</returns>
        public Dictionary<string, Trip> ParseTrips(string filename)
        {
            int tripLookupId = 1;
            return this.ParseFile<string, Trip, Trip>(
                filename,
                (tripData) =>
                {
                    return new Trip()
                    {
                        Id = tripData["trip_id"],
                        RouteId = tripData["route_id"],
                        ServiceId = tripData["service_id"],
                        LookupId = tripLookupId++
                    };
                },
                (trip) => trip.Id,
                (trip, t) => trip);
        }

        /// <summary>
        /// Parse stop_times
        /// </summary>
        /// <param name="filename">Filename with stop times</param>
        /// <param name="stops">Parsed stops</param>
        /// <param name="trips">Parsed trip data</param>
        /// <returns>Dictionary that maps stop lookup id to associated times</returns>
        public Dictionary<int, List<StopTime>> ParseStopTimes(
            string filename,
            Dictionary<string, Stop> stops,
            Dictionary<string, Trip> trips)
        {
            return this.ParseFile<int, StopTime, List<StopTime>>(
                filename,
                (stopTimeData) =>
                {
                    return new StopTime()
                    {
                        StopLookupId = stops[stopTimeData["stop_id"]].LookupId,
                        ArrivalTime = this.ParseGTFSTime(stopTimeData["arrival_time"]),
                        DepartureTime = this.ParseGTFSTime(stopTimeData["departure_time"]),
                        Sequence = Int32.Parse(stopTimeData["stop_sequence"], CultureInfo.InvariantCulture),
                        TripLookupId = trips[stopTimeData["trip_id"]].LookupId,
                        TrainId = stopTimeData["trip_id"].Split(new char[1] { '_' }, 2)[0]
                    };
                },
                (stopTime) => stopTime.StopLookupId,
                (stoptime, list) =>
                {
                    if (list == null)
                    {
                        list = new List<StopTime>();
                        list.Add(stoptime);
                    }
                    else
                    {
                        list.Add(stoptime);
                    }

                    return list;
                });
        }

        /// <summary>
        /// Parses a CSV file and flattens into an object structure. Any arbitrary value can be used for key.
        /// The value can also be a user specified aggregate/collection, values discovered with the same key
        /// are passed on to the value collapser.
        /// </summary>
        /// <typeparam name="T">Type of dictionary key when collapsed</typeparam>
        /// <typeparam name="U">Type of object represented in each row of CSV</typeparam>
        /// <typeparam name="V">Type of collapsed object when collapsing objects with same key</typeparam>
        /// <param name="filename">File to read input from</param>
        /// <param name="objectMapper">Method that maps row data to object</param>
        /// <param name="keySelector">Key selector used for extracting key from object</param>
        /// <param name="valueCollapser">Collapser method used for collapsing multiple objects with same key</param>
        /// <returns>Dictionary that represents CSV file</returns>
        private Dictionary<T, V> ParseFile<T, U, V>(
            string filename, 
            Func<Dictionary<string, string>, U> objectMapper, 
            Func<U, T> keySelector,
            Func<U, V, V> valueCollapser)
        {
            Dictionary<T, V> result = new Dictionary<T, V>();
            Dictionary<string, string> row = new Dictionary<string, string>();

            using (StreamReader reader = new StreamReader(filename))
            {
                string headerline = reader.ReadLine();
                string line;
                string[] keys = this.csvLineParser.ParseLine(headerline);
                foreach (string key in keys)
                {
                    row.Add(key, null);
                }

                while ((line = reader.ReadLine()) != null)
                {
                    string[] values = this.csvLineParser.ParseLine(line);
                    if (values.Length != keys.Length)
                    {
                        throw new ApplicationException(String.Format("Number of columns does not match header: {0}", line));
                    }

                    for (int i = 0; i < keys.Length; i++)
                    {
                        row[keys[i]] = values[i];
                    }

                    U mappedObject = objectMapper(row);
                    T key = keySelector(mappedObject);
                    if (result.ContainsKey(key))
                    {
                        result[key] = valueCollapser(mappedObject, result[key]);
                    }
                    else
                    {
                        result[key] = valueCollapser(mappedObject, default(V));
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Parses a given GTFS time value
        /// </summary>
        /// <param name="time">Time value to parse</param>
        /// <returns>Time value after parsing</returns>
        private TimeSpan ParseGTFSTime(string time)
        {
            string toparse;
            int daysToAdd = 0;
            int hour = Int32.Parse(time.Substring(0, 2));
            if (hour > 23)
            {
                daysToAdd = hour / 24;
                hour = hour % 24;
                toparse = hour.ToString("D2") + time.Remove(0, 2);
            }
            else
            {
                toparse = time;
            }

            var returnVal = TimeSpan.Parse(toparse, CultureInfo.InvariantCulture);
            if (daysToAdd > 0)
            {
                returnVal += TimeSpan.FromDays(daysToAdd);
            }

            return returnVal;
        }

        /// <summary>
        /// Transforms service days from CSV to dictionary format
        /// </summary>
        /// <param name="serviceDays">Dictionary from CSV to parse</param>
        /// <returns>Parsed dictionary</returns>
        private Dictionary<DayOfWeek, bool> ParseServiceDays(Dictionary<string, string> serviceDays)
        {
            Func<string, bool> isAvailable = (day) =>
                {
                    return (Int32.Parse(serviceDays[day]) == 0) ? false : true;
                };

            Dictionary<DayOfWeek, bool> days = new Dictionary<DayOfWeek, bool>();
            days.Add(DayOfWeek.Monday, isAvailable("monday"));
            days.Add(DayOfWeek.Tuesday, isAvailable("tuesday"));
            days.Add(DayOfWeek.Wednesday, isAvailable("wednesday"));
            days.Add(DayOfWeek.Thursday, isAvailable("thursday"));
            days.Add(DayOfWeek.Friday, isAvailable("friday"));
            days.Add(DayOfWeek.Saturday, isAvailable("saturday"));
            days.Add(DayOfWeek.Sunday, isAvailable("sunday"));

            return days;
        }
    }
}
